#include <vector>
#include <fstream>
#include <iostream>

#include "math/Vector3.h"
#include "mesh/OBJMeshLoader.h"
#include "loader/ResourceCollection.h"
#include "exceptions.h"
#include "mesh/MeshUtil.h"
#include "file/TokenizedFile.h"
#include "util/FileUtil.h"

using namespace std;

OBJMeshLoader::OBJMeshLoader(ResourceCollection *resources): MeshLoader(resources){}

bool OBJMeshLoader::supports(std::string path){
	string ext = FileUtil::getFileExtension(path);
	return ext == "obj";
}

void OBJMeshLoader::loadMaterialLibrary(string objPath, string matLibPath, vector<Material*> &materials,  vector<Texture*> &textures) {
	string directory = objPath.substr(0, objPath.find_last_of("/") + 1);
	string path = directory + matLibPath;

	Material *material = NULL;

	bool readMaterialLines = true;

	TokenizedFile file;
	file.open(path);

	while (file.hasLines()) {
		TokenLine line = file.getLine(' ');

		// Skip over comment lines and blank lines
		if (line.size() == 0 || line.getChars(0) == "#")
			continue;

		// Create a new material
		if (line.getChars(0) == "newmtl") {
			material = resources->newMaterial();
			material->setName(line.getChars(1));
			materials.push_back(material);
			readMaterialLines = true;
			continue;
		}

		if (readMaterialLines) {
//			if (line.getChars(0) == "Ka") {
//				material->setAmbient(RGBA(
//						line.getFloat(1),
//						line.getFloat(2),
//						line.getFloat(3),
//						1.0f));
//				continue;
//			}

			if (line.getChars(0) == "Kd") {
				material->setDiffuse(RGBA(
						line.getFloat(1),
						line.getFloat(2),
						line.getFloat(3),
						1.0f));
				continue;
			}

			if (line.getChars(0) == "Ks") {
				material->setSpecular(RGBA(
						line.getFloat(1),
						line.getFloat(2),
						line.getFloat(3),
						1.0f));
				continue;
			}

			if (line.getChars(0) == "Ns") {
				material->setShininess(line.getFloat(1));
				continue;
			}

			if (line.getChars(0) == "map_Kd") {
				Texture* texture = resources->loadTexture(directory + line.getString(1), true);
				textures.push_back(texture);
				TextureReference* texref = new TextureReference();
				texref->setTexture(texture);
				vector<TextureReference*> texrefs;
				texrefs.push_back(texref);
				material->setTexrefs(texrefs);
				continue;
			}
		}
	}
}

void parseVector(vector<Vector3> &vectors, const TokenLine &line) {
	if (line.size() == 4) {
		float x = line.getFloat(1);
		float y = line.getFloat(2);
		float z = line.getFloat(3);
		vectors.push_back(Vector3(x, y, z));
	}
	else if (line.size() == 3) {
		float x = line.getFloat(1);
		float y = line.getFloat(2);
		vectors.push_back(Vector3(x, y, 0));
	}
}

Mesh* OBJMeshLoader::load(string path, ShaderProgram* program) {
	//cout << "Loading " << path << endl;
	Material *currentMaterial = resources->getDefaultMaterial();

	string name = "";
	vector<Vector3> vertices;
	vector<Vector3> textureUVs;
	vector<Vector3> uvLayerCoords;
	vector<Vector3> vertexNormals;
	vector<Texture*> textures;
	vector<Material*> materials;
	vector<Face*> faces;

	TokenizedFile file;
	file.open(path);
	while (file.hasLines()) {
		TokenLine line = file.getLine(' ');

		// Skip over comment lines
		if (line.size() == 0 || line.getChars(0) == "#")
			continue;

		// If a mesh name is found, use it
		else if (line.getChars(0) == "o") {
			name = line.getChars(1);
		}

		// Create a vertex
		else if (line.getChars(0) == "v") {
			parseVector(vertices, line);
		}

		// Create a textureUV
		else if (line.getChars(0) == "vt") {
			parseVector(textureUVs, line);
		}

		// Create a vertex normal
		else if (line.getChars(0) == "vn") {
			parseVector(vertexNormals, line);
		}

		// Create a face
		else if (line.getChars(0) == "f") {
		
			// Make sure the face is a triangle or a quad
			if (line.size() != 4 && line.size() != 5) {
				continue;
			}

			Face *face = new Face;
			face->material = currentMaterial;

			// Split the face data into a usable line
			for (unsigned int i = 1; i < line.size(); i++) {

				// Only has a vertex index
				if (line.getString(i).find('/') == string::npos) {
					int index = line.getInt(i) - 1;
					face->vertices.push_back(index);
				}

				// Has texture/normal data
				else {
					TokenLine vertLine = TokenLine(line.getString(i), '/');

					if (!vertLine.isEmpty(0)) {

						// Add vertex index
						int index = vertLine.getInt(0) - 1;
						face->vertices.push_back(index);

						// Add textureUV index
						if (!vertLine.isEmpty(1)) {
							index = vertLine.getInt(1) - 1;
							uvLayerCoords.push_back(textureUVs[index]);
						}

						// Add vertexNormal index
						if (!vertLine.isEmpty(2)) {
							index = vertLine.getInt(2) - 1;
							face->vertexNormals.push_back(index);
						}

					}
				}
			}

			// If there is a quad it should be converted into 2 triangles
			if (face->vertices.size() == 4) {
				pair<Face*, Face*> tris = MeshUtil::triangulateQuad(face);
				faces.push_back(tris.first);
				faces.push_back(tris.second);
				delete face;
			}
			else {
				faces.push_back(face);
			}
		}

		else if (line.getChars(0) == "mtllib") {
			try {
				loadMaterialLibrary(path, line.getString(1), materials, textures);
				//cout << "Loaded materials from " << line.getString(1) << endl;
			} catch (FileException e) {
				//cout << e.what() << endl;
				//cout << "Ignoring missing materials and continuing." << endl;
			}
		}

		else if (line.getChars(0) == "usemtl") {
			vector<Material*>::iterator material;
			for (material = materials.begin(); material < materials.end(); material++) {
				if (line.getChars(1) == (*material)->getName())
					currentMaterial = (*material);
			}
		}
	}

	vector<UVLayer*> uvLayers;
	uvLayers.push_back(new UVLayer("default", uvLayerCoords));

	for (unsigned int i = 0; i < materials.size(); i++) {
		if (materials[i]->getTexrefs().size() > 0) {
			materials[i]->getTexrefs()[0]->setUvLayer(uvLayers[0]);
		}
	}

	Mesh *mesh = new Mesh(vertices, vertexNormals, textures, uvLayers, materials, faces, program);
	mesh->setName(name);
	mesh->compile();
	resources->put(mesh);
	return mesh;
}

OBJMeshLoader::~OBJMeshLoader() {}
